/*
 * Copyright 2012 Benoît Combe <benoit.combe@gmail.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fr.ipst.spot.facades.entites;

import fr.ipst.spot.entites.AbstractSpotEntite;
import fr.ipst.spot.exceptions.SpotExceptionEjb;
import fr.ipst.spot.formulaires.entites.FormAbstractHtmlEntite;
import fr.ipst.spot.formulaires.entites.FormAbstractHtmlEntiteId;
import fr.ipst.spot.formulaires.entites.utils.FormDateBeforeAfter;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import javax.ejb.EJBException;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

/**
 *
 * @param <T>
 * @author Benoît Combe <benoit.combe@gmail.com>
 */
public abstract class AbstractFacade<T extends AbstractSpotEntite> {

    /**
     *
     */
    protected Class<T> entityClass;
    /**
     * HashMap servant à stocker les paramètres des requêtes nommées
     */
    protected HashMap<String, Object> parametres = new HashMap<String, Object>(5);

    /**
     * Rajoute % % à la chaine passée en paramètre afin de faire des recherches
     * de type contenant en SQL
     *
     * @param toPrepare La chaine à modifier
     * @return La chaine passée en paramètre préfixée de % et suffixée de %
     */
    protected String getPatternLike(String toPrepare) {
        return "%" + toPrepare + "%";
    }

    /**
     * Constructeur unique, prend en pramaètre la Classe pour laquelle la facade
     * est spécialsée.
     *
     * @param entityClass La Classe pour laquelle l'interface est spécialisée
     */
    public AbstractFacade(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    /**
     * Méthode abstraite récupère un EntityManager spécialisée sur la Classe de
     * la facade
     *
     * @return Un EntityManager spécialisée sur la Classe de la facade
     */
    protected abstract EntityManager getEntityManager();

    /**
     * Enregistre un objet de la Classe spécialisée dans la base. <ul>
     * <li>Vérifie que les données transmises respectent les critères
     * d'intégrité</li> <li>Ecrit l'objet dans la base et retourne l'objet
     * créé</li> </ul>
     *
     * @param formAbstractHtmlEntite Le Form contenant les données de l'objet à
     * persister
     * @return L'objet créé
     * @throws SpotExceptionEjb
     */
    protected T create(FormAbstractHtmlEntite<T> formAbstractHtmlEntite) throws SpotExceptionEjb {
        try {
            // Vérification dse données transmises
            formAbstractHtmlEntite.validateCreation();
            // Récupération de l'objet à persister
            T entity = formAbstractHtmlEntite.getEntityToPersist();
            // Execution de la persistance
            getEntityManager().persist(entity);
            // Retour de l'objet perssité : son Id est mis à jour
            return entity;
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Modifie un enregistrement de la Classe spécialisée dans la base. <ul>
     * <li>Vérifie que les données transmises respectent les critères
     * d'intégrité</li> <li>Ecrit les modifications dans la base et retourne
     * l'objet mis à jour</li></ul>
     *
     * @param formAbstractHtmlEntite Form contenant les données de mise à jour
     * de l'objet
     * @return L'objet mise à jour
     * @throws SpotExceptionEjb
     */
    protected T edit(FormAbstractHtmlEntite<T> formAbstractHtmlEntite) throws SpotExceptionEjb {
        try {
            // Vérification dse données transmises
            formAbstractHtmlEntite.validateModification();
            // Récupération de l'objet à mettre à jour
            T toUpdate = getEntityManager().find(entityClass, formAbstractHtmlEntite.getId());
            // Si l'objet existe
            if (toUpdate != null) {
                // Execution de la requête de mise à jour et retour de l'objet mis à jour
                return getEntityManager().merge(formAbstractHtmlEntite.getEntityToUpdate(toUpdate));
            } else {
                // Sinon Exception
                throw new SpotExceptionEjb(this.entityClass.getName() + " : Tentative de mise à jour d'un objet n'existant pas");
            }
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Supprime l'objet de la base de la Classe spécialisée en fonction de son
     * id passé en paramètre
     *
     * @param formAbstractHtmlEntiteId Le form contenant l'id de l'objet à
     * supprimer
     * @throws SpotExceptionEjb
     */
    protected void remove(FormAbstractHtmlEntiteId<T> formAbstractHtmlEntiteId) throws SpotExceptionEjb {
        try {
            formAbstractHtmlEntiteId.validate();
            // Execution de la requête de suppression
            getEntityManager().remove(getEntityManager().merge(getEntityManager().find(entityClass, formAbstractHtmlEntiteId.getId())));
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Remonte un objet de la Classe spécialisée depuis la base en fonction de
     * l'id passé en paramètre
     *
     * @param formAbstractHtmlEntiteId LLe form contenant l'id de l'objet
     * recherché
     * @return L'objet recherché
     * @throws SpotExceptionEjb
     */
    protected T find(FormAbstractHtmlEntiteId<T> formAbstractHtmlEntiteId) throws SpotExceptionEjb {
        try {
            formAbstractHtmlEntiteId.validate();
            // Execution de la requête et retour de l'objet recherché
            return getEntityManager().find(entityClass, formAbstractHtmlEntiteId.getId());
        } catch (EJBException e) {
            // Si erreur d'execution SQL : Log
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            // Si autre erreur : Log
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Remonte une liste contenant l'ensemble des enregistrements en base pour
     * la Classe spéciaisée
     *
     * @return Une liste contenant l'ensemble des enregistrements en base
     * @throws SpotExceptionEjb
     */
    public List<T> findAll() throws SpotExceptionEjb {
        try {
            // Création de la requête Criteria
            CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
            // Ajout de la clause FROM
            cq.select(cq.from(entityClass));
            // Execution de la requête et retour des résultats
            return getEntityManager().createQuery(cq).getResultList();
        } catch (EJBException e) {
            // Si erreur d'execution SQL : Log
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            // Si autre erreur : Log
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Remonte une liste contenant l'ensemble des enregistrements en base pour
     * la Classe spéciaisée dont l'indice est compris entre le premier et le
     * deuxième entier du tableau en paramètre (LIMIT en SQL)
     *
     * @param range Tableau contenant le premier indice et l'indice max à
     * remonter
     * @return Une liste contenant l'ensemble des enregistrements en base dont
     * l'indice est compris entre le premier et le deuxième entier du tableau en
     * paramètre
     * @throws SpotExceptionEjb
     */
    public List<T> findRange(int[] range) throws SpotExceptionEjb {
        try {
            // Création de la requête Criteria
            CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
            // Ajout de la clause FROM
            cq.select(cq.from(entityClass));
            // Création d'une query portant la requête Criteria
            Query q = getEntityManager().createQuery(cq);
            // Ajout du nombre max d'enregistrement
            q.setMaxResults(range[1] - range[0]);
            // Ajout du premier indice à remonter
            q.setFirstResult(range[0]);
            // Execution de la requêt et mise à jour du résultat
            return q.getResultList();
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Execute un COUNT de tous les enregistrements de la base pour la Classe
     * spéciaisée
     *
     * @return la valeur du COUNT de tous les les enregistrements de la base
     * pour la Classe spéciaisée
     * @throws SpotExceptionEjb
     */
    public int count() throws SpotExceptionEjb {
        try {
            // Création de la requête Criteria
            CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
            // Ajout du ROOT de la requête Criteria
            Root<T> rt = cq.from(entityClass);
            // Ajout du type requête COUNT dans la requête Criteria
            cq.select(getEntityManager().getCriteriaBuilder().count(rt));
            // Création de la query portant la requête Criteria
            Query q = getEntityManager().createQuery(cq);
            // Execution de la requête et retour du résultat
            return ((Long) q.getSingleResult()).intValue();
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Méthode privée : créer une TypedQuery en portant la NamedQuery dont le
     * nom est passé en paramètre et bind les paramètres présents dans la
     * HashMap paramètes, remet la HashMap paramètes à zéro.
     *
     * @param namedQueryName Le nom de la NamedQuery à executer
     * @return la TypeQuery portant la NamedQuery à executer avec l'ensemble des
     * paramètres bindés
     */
    private TypedQuery createNamedQueryWithParametres(String namedQueryName) {
        // Création d'une TypedQuery pour la Classe spécialisée
        TypedQuery query = getEntityManager().createNamedQuery(namedQueryName, entityClass);
        // Bind des paramètres dans la TypeQuery :
        // Pour chaque entrée dans la HashMap paramètres
        for (Entry<String, Object> e : parametres.entrySet()) {
            // Mise à jour du paramètre dans la TypedQuery
            query.setParameter(e.getKey(), e.getValue());
        }
        // Remise à zéro de la HashMap paramètres
        this.parametres = new HashMap<String, Object>(5);
        // Retour de la TypedQuery
        return query;
    }

    /**
     * Méthode privée : ajoute les critères de limite au résultat de la
     * TypedQuery passée en paramètre (LIMIT en SQL)
     *
     * @param typedQuery La TypedQuery à limiter
     * @param range Tableau contenant le premier indice et l'indice max à
     * remonter
     * @return La TypedQuery avec les conditions de LIMIT ajoutées
     * @throws SpotExceptionEjb
     */
    private TypedQuery addLimitToQuery(TypedQuery typedQuery, int[] range) throws SpotExceptionEjb {
        try {
            // Ajout du nombre max d'enregistrements
            typedQuery.setMaxResults(range[1] - range[0]);
            // Ajout
            typedQuery.setFirstResult(range[0]);
            return typedQuery;
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Remonte une liste contenant tous les enregistrements en base de la Classe
     * spécialisée correspondant à la NamedQuery passée en paramètre
     * <br/><strong>Les paramètres de la NamedQuery doivent avoir été ajoutés en
     * amont à la HashMap paramètres</strong>
     *
     * @param namedQuerryName Le nom de la NamedQuey à executer
     * @return Une liste contenant tous les enregistrements en base de la Classe
     * spécialisée correspondant à la NamedQuery passée en paramètre
     * @throws SpotExceptionEjb
     */
    protected List<T> findByNamedQuerry(String namedQuerryName) throws SpotExceptionEjb {
        // Execution de la méthode
        return this.createNamedQueryWithParametres(namedQuerryName).getResultList();
    }

    /**
     * Remonte une liste contenant tous les enregistrements en base de la Classe
     * spécialisée dont l'index est compris entre le premier et le deuxième
     * entier du tableau passé en paramètre correspondant à la NamedQuery passée
     * en paramètre<br/><strong>Les paramètres de la NamedQuery doivent avoir
     * été ajoutés en amont à la HashMap paramètres</strong>
     *
     * @param namedQuerryName Le nom de la NamedQuery à executer
     * @param range Tableau contenant le premier indice et l'indice max à
     * remonter
     * @return Une liste contenant tous les enregistrements en base de la Classe
     * spécialisée dont l'index est compris entre le premier et le deuxième
     * entier du tableau passé en paramètre correspondant à la NamedQuery passée
     * en paramètre
     * @throws SpotExceptionEjb
     */
    protected List<T> findByNamedQuerry(String namedQuerryName, int[] range) throws SpotExceptionEjb {
        try {
            // Création d'une TypedQuery pour la Classe spécialisée
            TypedQuery querry = this.createNamedQueryWithParametres(namedQuerryName);
            // Ajout des paramètres de LIMIT, execution de la requête et retour du résultat
            return this.addLimitToQuery(querry, range).getResultList();
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Remonte une liste contenant tous les enregistrements en base de la Classe
     * spécialisée dont l'index est compris entre le premier et le deuxième
     * entier du tableau passé en paramètre en fonction des dates passées en
     * paramètre correspondant à la NamedQuery passée en paramètre
     * <br/><strong>Les paramètres de la NamedQuery doivent avoir été ajoutés en
     * amont à la HashMap paramètres</strong>
     *
     * @param namedQuerryName Le nom de la NamedQuery à executer
     * @param range Tableau contenant le premier indice et l'indice max à
     * remonter
     * @param formDateBeforeAfter Form contenant les dates de la période visée
     * @return Une liste contenant tous les enregistrements en base de la Classe
     * spécialisée dont l'index est compris entre le premier et le deuxième
     * entier du tableau passé en paramètre en fonction des dates passées en
     * paramètre correspondant à la NamedQuery passée en paramètre
     * @throws SpotExceptionEjb
     */
    protected List<T> findByNamedQuerryByDates(String namedQuerryName, int[] range, FormDateBeforeAfter formDateBeforeAfter) throws SpotExceptionEjb {
        try {
            // Création de la TypedQuery correspondant à la NamedQuery
            TypedQuery typedQuery = this.createNamedQueryWithParametres(namedQuerryName);
            // Ajout des paramètres de dates
            typedQuery.setParameter("before", formDateBeforeAfter.getBeforeDate());
            typedQuery.setParameter("after", formDateBeforeAfter.getAfterDate());
            // Ajout des condititons de LIMIT, execution de la requête et retour du résultat
            return this.addLimitToQuery(typedQuery, range).getResultList();
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     * Remonte un enregistrement de la Classe spécialisée en base correspondant
     * à la NamedQuery dont le nom est passé en paramètre<br/><strong>Les
     * paramètres de la NamedQuery doivent avoir été ajoutés en amont à la
     * HashMap paramètres</strong>
     *
     * @param namedQuerryName Le nom de la NamedQuey a executer
     * @return L'objet de la Classe spécialisée correspondant en base à la
     * NamedQuery exectuée
     * @throws SpotExceptionEjb
     */
    protected T findOneByNamedQuerry(String namedQuerryName) throws SpotExceptionEjb {
        try {
            // Création de TypedQuery portant la NamedQuery, execution de la requête, cast et retour du résultat
            return (T) this.createNamedQueryWithParametres(namedQuerryName).getSingleResult();
        } catch (NoResultException e) {
            return null;
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }

    /**
     *
     * @param namedQueryName
     * @return
     * @throws SpotExceptionEjb
     */
    protected int runUpdateDeleteNamedQuery(String namedQueryName) throws SpotExceptionEjb {
        try {
            // Création de TypedQuery portant la NamedQuery, execution de la requête
            return this.createNamedQueryWithParametres(namedQueryName).executeUpdate();
        } catch (EJBException e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        } catch (Exception e) {
            throw new SpotExceptionEjb(e.getMessage(), e);
        }
    }
}